package com.sunday.authorization.mock.oauth2.server.authorization;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sunday.authorization.mock.oauth2.server.authorization.redis.module.OAuth2AuthorizationModule;
import com.sunday.authorization.mock.oauth2.server.authorization.redis.restrict.ConcurrentUserLogin;
import com.sunday.authorization.mock.oauth2.server.constants.RedisOAuth2Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.converter.json.SpringHandlerInstantiator;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.*;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.util.Assert;

import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * Redis authorization service
 *
 * 首先此方法并没有考虑全部的授权方式，仅已 org.springframework.security.oauth2.core.AuthorizationGrantType 中的模式作为参考
 *
 * https://docs.spring.io/spring-authorization-server/reference/core-model-components.html#oauth2-authorization-service
 * OAuth2AuthorizationService
 * 是存储新授权和查询现有授权的中心组件。 在遵循特定协议流程时，其他组件会使用它，例如，客户端身份验证、授权处理、令牌自省、令牌撤销、动态客户端注册等。OAuth2AuthorizationService
 * 提供的实现是 和 。 该实现将实例存储在内存中，建议仅在开发和测试期间使用。 是一个 JDBC 实现，它使用 持久化实例。OAuth2AuthorizationServiceInMemoryOAuth2AuthorizationServiceJdbcOAuth2AuthorizationServiceInMemoryOAuth2AuthorizationServiceOAuth2AuthorizationJdbcOAuth2AuthorizationServiceOAuth2AuthorizationJdbcOperations
 *
 * oauth2_authorization 表中每个字段含义的解释：
 * id：此表中每条授权记录的唯一标识符，数据类型为 varchar(100) 且不为空。
 * registered_client_id：与该授权相关的已注册客户端的标识符，varchar(100) 且不为空。
 * principal_name：授权所关联的主体名称，可能是用户的名称，varchar(200) 且不为空。
 * authorization_grant_type：授权的授予类型，例如授权码、密码等，varchar(100) 且不为空。
 * authorized_scopes：授权的范围，以字符串形式存储，可能包含多个范围，默认值为 NULL 。
 * attributes：可能包含与授权相关的一些其他属性，数据类型为 blob ，默认值为 NULL 。
 * state：授权的状态信息，varchar(500) ，默认值为 NULL 。
 * authorization_code_value：授权码的值，数据类型为 blob ，默认值为 NULL 。
 * authorization_code_issued_at：授权码的发放时间，时间戳类型，默认值为 NULL 。
 * authorization_code_expires_at：授权码的过期时间，时间戳类型，默认值为 NULL 。
 * authorization_code_metadata：授权码的元数据，数据类型为 blob ，默认值为 NULL 。
 * access_token_value：访问令牌的值，数据类型为 blob ，默认值为 NULL 。
 * access_token_issued_at：访问令牌的发放时间，时间戳类型，默认值为 NULL 。
 * access_token_expires_at：访问令牌的过期时间，时间戳类型，默认值为 NULL 。
 * access_token_metadata：访问令牌的元数据，数据类型为 blob ，默认值为 NULL 。
 * access_token_type：访问令牌的类型，varchar(100) ，默认值为 NULL 。
 * access_token_scopes：访问令牌的范围，varchar(1000) ，默认值为 NULL 。
 * oidc_id_token_value：OpenID Connect 身份令牌的值，数据类型为 blob ，默认值为 NULL 。
 * oidc_id_token_issued_at：OpenID Connect 身份令牌的发放时间，时间戳类型，默认值为 NULL 。
 * oidc_id_token_expires_at：OpenID Connect 身份令牌的过期时间，时间戳类型，默认值为 NULL 。
 * oidc_id_token_metadata：OpenID Connect 身份令牌的元数据，数据类型为 blob ，默认值为 NULL 。
 * refresh_token_value：刷新令牌的值，数据类型为 blob ，默认值为 NULL 。
 * refresh_token_issued_at：刷新令牌的发放时间，时间戳类型，默认值为 NULL 。
 * refresh_token_expires_at：刷新令牌的过期时间，时间戳类型，默认值为 NULL 。
 * refresh_token_metadata：刷新令牌的元数据，数据类型为 blob ，默认值为 NULL 。
 * user_code_value：用户代码的值，数据类型为 blob ，默认值为 NULL 。
 * user_code_issued_at：用户代码的发放时间，时间戳类型，默认值为 NULL 。
 * user_code_expires_at：用户代码的过期时间，时间戳类型，默认值为 NULL 。
 * user_code_metadata：用户代码的元数据，数据类型为 blob ，默认值为 NULL 。
 * device_code_value：设备代码的值，数据类型为 blob ，默认值为 NULL 。
 * device_code_issued_at：设备代码的发放时间，时间戳类型，默认值为 NULL 。
 * device_code_expires_at：设备代码的过期时间，时间戳类型，默认值为 NULL 。
 * device_code_metadata：设备代码的元数据，数据类型为 blob ，默认值为 NULL 。
 *
 * @author sunday
 * @since 2024/8/8
 *
 * @see InMemoryOAuth2AuthorizationService
 * @see JdbcOAuth2AuthorizationService
 */
@Slf4j
//@RequiredArgsConstructor
public class RedisOAuth2AuthorizationService implements OAuth2AuthorizationService {

    private StringRedisTemplate stringRedisTemplate;
    private RegisteredClientRepository registeredClientRepository;
    private int loginMax;
    private ObjectMapper objectMapper = new ObjectMapper();

    String HASH = "#";

    /**
     * @param stringRedisTemplate
     * @param registeredClientRepository
     * @param beanFactory                   自动装配
     * @param loginMax                      最大同时登录
     */
    public RedisOAuth2AuthorizationService(StringRedisTemplate stringRedisTemplate,
                                           RegisteredClientRepository registeredClientRepository,
                                           AutowireCapableBeanFactory beanFactory,
                                           int loginMax

    ) {
        this.loginMax = loginMax;
        this.stringRedisTemplate = stringRedisTemplate;
        this.registeredClientRepository = registeredClientRepository;

        ClassLoader loader = getClass().getClassLoader();
        List<Module> modules = SecurityJackson2Modules.getModules(loader);
        this.objectMapper.registerModules(modules);
        /**
         * 添加spring默认已实现支持
         */
        this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
        /**
         * 添加项目所需要得自定义实现
         */
        this.objectMapper.registerModule(new OAuth2AuthorizationModule());
        /**
         * 这个问题可能是由于在没有 Spring 上下文的支持下实例化反序列化器所导致的。通过添加 SpringHandlerInstantiator，您可以确保 Jackson 在反序列化过程中能够正确地通过 Spring 的 beanFactory 实例化反序列化器。
         * 在您的代码中，通过设置 SpringHandlerInstantiator，Spring 的 beanFactory 能够帮助实例化这些没有默认构造函数的类，解决了实例化反序列化器失败的问题。下面详细解释一下为什么这样的设置会有效：
         * 1. 为什么缺少无参构造函数会导致反序列化失败
         * Jackson 默认在反序列化时会尝试使用无参构造函数创建对象。如果类没有无参构造函数，就会引发实例化异常。而使用 SpringHandlerInstantiator 可以告诉 Jackson 使用 Spring 的上下文创建这些对象，这样如果这些对象依赖于某些 Bean 或有参构造函数，Spring 的上下文就能够提供这些依赖。
         * 2. SpringHandlerInstantiator 的使用
         * SpringHandlerInstantiator 能够让 Jackson 与 Spring 的 BeanFactory 协同工作，确保需要特定构造函数或依赖注入的类能够被正确实例化。
         */
        this.objectMapper.setHandlerInstantiator(new SpringHandlerInstantiator(beanFactory));
    }

    @Override
    public void save(OAuth2Authorization authorization) {

        Assert.notNull(authorization, "authorization cannot be null");
        Assert.hasText(authorization.getPrincipalName(), "principal name must not be empty");

        // 获取authorizationId
        String idToAuthorizationKey = RedisOAuth2Constants.getIdToAuthorization(authorization.getId());
        /**
         * 排定是授权完成，还是正在授权的步骤
         */
        if (isComplete(authorization)) {
            /**
             * 正常验证登录后修成
             */
            OAuth2AccessToken oAuth2AccessToken = authorization.getAccessToken().getToken();
            /**
             * 授权后可能存在 /oauth2/revoke，需要进行资源的释放动作
             * 1. token = refreshToken
             * 2. token = accessToken
             */
            if (authorization.getAccessToken().isInvalidated() ||
                (authorization.getRefreshToken() != null && authorization.getRefreshToken().isInvalidated())) {
                /**
                 * 登出流程, 优先删除当前登录信息
                 */
                stringRedisTemplate.opsForZSet().remove(RedisOAuth2Constants.getPrincipalName(authorization.getPrincipalName()), oAuth2AccessToken.getTokenValue());

                /**
                 * 当 accessToken 关闭了，且 refreshToken 并不存在则连同 idToAuthorizationKey 一并删除
                 */
                Optional.ofNullable(authorization.getAccessToken())
                        .filter(OAuth2Authorization.Token::isInvalidated)
                        .ifPresent(accessTokenToken -> {
                            stringRedisTemplate.delete(RedisOAuth2Constants.getAccessToAuthorization(accessTokenToken.getToken().getTokenValue()));
                            if (authorization.getRefreshToken() == null) {
                                stringRedisTemplate.delete(idToAuthorizationKey);
                            }
                        });
                /**
                 * 当 refreshToken != null && isInvalidated ，连同 idToAuthorizationKey 一并删除
                 */
                Optional.ofNullable(authorization.getRefreshToken())
                        .filter(OAuth2Authorization.Token::isInvalidated)
                        .ifPresent(refreshTokenToken -> {
                            stringRedisTemplate.delete(RedisOAuth2Constants.getRefreshToAuthorization(refreshTokenToken.getToken().getTokenValue()));
                            stringRedisTemplate.delete(idToAuthorizationKey);
                        });
            } else {
                /**
                 * 登录成功，验证用户同时登录数量
                 */
                ConcurrentUserLogin.getInstances().restriction(
                        RedisOAuth2Constants.getPrincipalName(authorization.getPrincipalName()),
                        oAuth2AccessToken.getTokenValue(),
                        Optional.ofNullable(authorization.getRefreshToken())
                                .map(OAuth2Authorization.Token::getToken)
                                .map(OAuth2RefreshToken::getTokenValue)
                                .map(RedisOAuth2Constants::getRefreshToAuthorization)
                                .orElse(null),
                        loginMax,
                        oAuth2AccessToken.getExpiresAt().getEpochSecond(),
                        getTokenExpiry(oAuth2AccessToken.getExpiresAt())
                );

                /**
                 * 授权已完成包含 authorization_code/client_credentials/refresh_token 等情况
                 * 1. 可能存在 refreshToken 的情况，需要看授权验证方式, 如果存在的话 那么授权保存时间应该已refreshToken的过期时间作为缓存过期时间，而不是已accessToken的过期时间
                 * 2. 一定会存在 accessToken 毕竟上面都判断完了
                 */
                Optional.ofNullable(authorization.getRefreshToken())
                        .map(OAuth2Authorization.Token::getToken)
                        .ifPresentOrElse(oAuth2RefreshToken -> {
                            long secondsDifference = getTokenExpiry(oAuth2RefreshToken.getExpiresAt());
                            set(idToAuthorizationKey, write(authorization), secondsDifference);
                            // [refresh token] - [id]
                            set(RedisOAuth2Constants.getRefreshToAuthorization(oAuth2RefreshToken.getTokenValue()), STR."\{authorization.getId()}\{HASH}\{oAuth2AccessToken.getTokenValue()}", secondsDifference);
                        }, () -> {
                            set(idToAuthorizationKey, write(authorization), getTokenExpiry(oAuth2AccessToken.getExpiresAt()));
                        });
                // 2. 一定会存在 accessToken 毕竟上面都判断完了
                // [refresh token] - [id]
                set(RedisOAuth2Constants.getAccessToAuthorization(oAuth2AccessToken.getTokenValue()), authorization.getId(), getTokenExpiry(oAuth2AccessToken.getExpiresAt()));
            }

        } else {
            /**
             * 授权未完成包含
             * 1. 可能存在 获取state情况，已支持用户跳转出授权确认页面
             * 2. 存在 OAuth2AuthorizationCode 授权码等待置换token，此时token尚未生成
             */
            Optional.ofNullable(authorization.getToken(OAuth2AuthorizationCode.class))
                    .map(OAuth2Authorization.Token::getToken)
                    .ifPresentOrElse(oAuth2AuthorizationCode -> {
                        /**
                         * 考虑到 2. 存在 OAuth2AuthorizationCode 授权码等待置换token，此时token尚未生成
                         * 所以采用 oAuth2AuthorizationCode.getExpiresAt() 计算当前的剩余时间
                         */
                        long secondsDifference = getTokenExpiry(oAuth2AuthorizationCode.getExpiresAt());
                        set(idToAuthorizationKey, write(authorization), secondsDifference);
                        // [code token] - [id]
                        set(RedisOAuth2Constants.getCodeToAuthorization(oAuth2AuthorizationCode.getTokenValue()), authorization.getId(), secondsDifference);
                    }, () -> {
                        // 获取客户端信息
                        String registeredClientId = authorization.getRegisteredClientId();
                        RegisteredClient registeredClient = this.registeredClientRepository.findById(registeredClientId);
                        Assert.notNull(registeredClient, "registered client cannot be null");
                        Assert.notNull(registeredClient.getTokenSettings(), "registered client token settings cannot be null");
                        /**
                         * 考虑到 1. 可能存在 获取state情况，已支持用户跳转出授权确认页面
                         * 所以采用 TokenSettings().getAuthorizationCodeTimeToLive() 计算当前的剩余时间
                         */
                        Duration codeTimeToLive = registeredClient.getTokenSettings().getAuthorizationCodeTimeToLive();
                        set(idToAuthorizationKey, write(authorization), codeTimeToLive.getSeconds());
                        // [state] - [id]
                        set(RedisOAuth2Constants.getStateToAuthorization(authorization.getAttribute(OAuth2ParameterNames.STATE)), authorization.getId(), codeTimeToLive.getSeconds());
                    });
        }
    }

    @Override
    public void remove(OAuth2Authorization authorization) {
        Assert.notNull(authorization, "authorization cannot be null");
        stringRedisTemplate.delete(RedisOAuth2Constants.getIdToAuthorization(authorization.getId()));
    }

    @Override
    public OAuth2Authorization findById(String id) {
        Assert.hasText(id, "id cannot be empty");
        return Optional.ofNullable(get(RedisOAuth2Constants.getIdToAuthorization(id)))
                .map(this::parse)
                .orElse(null);
    }

    @Override
    public OAuth2Authorization findByToken(String token, OAuth2TokenType tokenType) {
        Assert.hasText(token, "token cannot be empty");
        if (tokenType == null) {
            return Optional.ofNullable(get(RedisOAuth2Constants.getStateToAuthorization(token)))
                    .or(() -> Optional.ofNullable(get(RedisOAuth2Constants.getCodeToAuthorization(token))))
                    .or(() -> Optional.ofNullable(get(RedisOAuth2Constants.getAccessToAuthorization(token))))
                    .or(() -> Optional.ofNullable(get(RedisOAuth2Constants.getRefreshToAuthorization(token)))
                            .map(value -> value.split(HASH))
                            .filter(values -> values.length == 2)
                            .map(values -> values[0]))
                    .map(this::findById)
                    .orElse(null);
        } else if (OAuth2ParameterNames.STATE.equals(tokenType.getValue())) {
            return Optional.ofNullable(get(RedisOAuth2Constants.getStateToAuthorization(token))).map(this::findById).orElse(null);
        } else if (OAuth2ParameterNames.CODE.equals(tokenType.getValue())) {
            return Optional.ofNullable(get(RedisOAuth2Constants.getCodeToAuthorization(token))).map(this::findById).orElse(null);
        } else if (OAuth2TokenType.ACCESS_TOKEN.equals(tokenType)) {
            return Optional.ofNullable(get(RedisOAuth2Constants.getAccessToAuthorization(token))).map(this::findById).orElse(null);
        } else if (OAuth2TokenType.REFRESH_TOKEN.equals(tokenType)) {
            return Optional.ofNullable(get(RedisOAuth2Constants.getRefreshToAuthorization(token)))
                    .map(value -> value.split(HASH))
                    .filter(values -> values.length == 2)
                    .map(values -> values[0])
                    .map(this::findById)
                    .orElse(null);
        }
        return null;
    }


    private static boolean isComplete(OAuth2Authorization authorization) {
        return authorization.getAccessToken() != null;
    }

    /**
     * 获取Token过期时间
     */
    private static long getTokenExpiry(Instant futureInstant) {
        // 计算秒差
        long secondsDifference = Duration.between(Instant.now(), futureInstant).getSeconds();
        if (log.isDebugEnabled()) {
            log.debug(STR."距离未来指定时间的秒差: \{secondsDifference}");
        }
        return secondsDifference;
    }

    /**
     * 将OAuth2Authorization对象序列化成字符串
     *
     * @param data  OAuth2Authorization对象
     * @return OAuth2Authorization字符串
     * @see JdbcOAuth2AuthorizationService.OAuth2AuthorizationParametersMapper#writeMap(Map)
     */
    private String write(Object data) {
        try {
            return this.objectMapper.writeValueAsString(data);
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex.getMessage(), ex);
        }
    }

    /**
     * 将redis里的OAuth2Authorization反序列化成OAuth2Authorization对象
     *
     * @param data  OAuth2Authorization序列化串
     * @return OAuth2Authorization
     * @see JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper#parseMap(String)
     */
    private OAuth2Authorization parse(String data) {
        try {
            return this.objectMapper.readValue(data, new TypeReference<>() {
            });
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex.getMessage(), ex);
        }
    }

    private void set(String key, String value, long timeout) {
        stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    private String get(Object key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

}
